import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;

import org.newdawn.slick.opengl.Texture;
import org.newdawn.slick.opengl.TextureLoader;

public class ObjectCoords
{
	private int numDimensions;
	private Scanner scan;

	private Texture ambientTex; //This is the texture associated with the OBJ file, which should be linked to the mtl file. Currently, parsing through the mtl file does not work.

	private ArrayList<double[]> vertexList = new ArrayList<double[]>(0); //Unordered groups of coordinates (x, y, z)
	private ArrayList<double[]> textureList = new ArrayList<double[]>(0); //This is an unordered group of texture coordinates (u, v)
	private ArrayList<double[]> orderedVertices = new ArrayList<double[]>(0); //These are ordered
	private ArrayList<double[]> orderedTextures = new ArrayList<double[]>(0); //Ordered
	private ArrayList<double[]> faceNormals = new ArrayList<double[]>(0); //These are the normals associated with a face, and are calculated manually
	private ArrayList<ArrayList<Integer>> faceVectorAllocate = new ArrayList<ArrayList<Integer>>(0); //This stores three vertices at a time, in order to make a triangle. They are ordered to work correctly with backface culling
	private ArrayList<double[]> vertexNormals = new ArrayList<double[]>(0); //These are the smoothed normals associated with a vertex, which are smoothed and normalized by averaging face normals.
	private ArrayList<double[]> vertexFaceNormals = new ArrayList<double[]>(0); //These correlate faces with normals
	private ArrayList<Face> faceList = new ArrayList<Face>(); //This is the combined list.

	public int getNumFaces()
	{
		return orderedVertices.size();
	}
	
	public double getVectorForFace(int face, int vertexNum)
	{
		return orderedVertices.get(face)[vertexNum];
	}
	
	public double getTextureForFace(int face, int texNum)
	{
		return orderedTextures.get(face)[texNum];
	}
	
	public double getNormalForFace(int face, int normalNum)
	{
		return vertexFaceNormals.get(face)[normalNum];
	}
	
	//The string is the name of the obj file, without the extension
	public ObjectCoords(String name)
	{
		try
		{
			scan = new Scanner(new File((name + ".obj")));
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		}
		//Reads until EOF, Scanner is easier to use than a BufferedReader, although changes may be made for improved speed.
		while(scan.hasNext())
		{
			String curLine = scan.nextLine();
			if(curLine.length() > 2 && !curLine.startsWith("mtl"))
			{
				int curChar = 2; //Starts after declaration of the type of data is being inputed (e.g. "v " or "f ", later "vt " will add 1 to curChar)
				if(curLine.substring(0, 2).equals("v "))
				{
					faceVectorAllocate.add(new ArrayList<Integer>());
					vertexList.add(new double[3]); //Adding to the real unordered vertexList
					String coordX = "", coordY = "", coordZ = "";
					//The following reads in a line such as: "v 1 2 3". Should be upgraded to use a StringTokenizer
					while(curLine.charAt(curChar) != ' ')
					{
						coordX += curLine.charAt(curChar);
						curChar++;
					}
					vertexList.get(vertexList.size() - 1)[0] = Double.parseDouble(coordX); //Assigns x coordinate to unordered list
					curChar++;
					while(curLine.charAt(curChar) != ' ')
					{
						coordY += curLine.charAt(curChar);
						curChar++;
					}
					vertexList.get(vertexList.size() - 1)[1] = Double.parseDouble(coordY); //Assigns y
					curChar++;
					while(curChar < curLine.length())
					{
						coordZ += curLine.charAt(curChar);
						curChar++;
					}
					vertexList.get(vertexList.size() - 1)[2] = Double.parseDouble(coordZ); //Assigns z
				}
				//Assigns vertex texture coordinates
				else if(curLine.substring(0, 3).equals("vt "))
				{
					curChar++; //Adds one character because "vt " has one more character
					textureList.add(new double[2]); //Only two coordinates because textures are assigned to a two-dimensional image
					String coordX = "", coordY = "";
					//Like previous, should be upgraded to a StringTokenizer
					while(curLine.charAt(curChar) != ' ')
					{
						coordX += curLine.charAt(curChar);
						curChar++;
					}
					textureList.get(textureList.size() - 1)[0] = Double.parseDouble(coordX);
					while(curChar < curLine.length())
					{
						coordY += curLine.charAt(curChar);
						curChar++;
					}
					textureList.get(textureList.size() - 1)[1] = Double.parseDouble(coordY);
				}
				//Assigns textures and vertices to their corresponding face
				else if(curLine.substring(0, 2).equals("f "))
				{
					String firstCoords = "", secondCoords = "", thirdCoords = "";
					String firstTex = "", secondTex = "", thirdTex = "";
					int xyz = 0;
					int vertexTextureNormal = 0;
					boolean hasTex = false;
					orderedVertices.add(new double[9]);
					orderedTextures.add(new double[6]);
					//Searches through and assigns vertices to texture coordinates, like so: "f v/t v/t v/t"
					for(int i = 2; i < curLine.length(); i++)
					{
						if(curLine.charAt(i) == '/' || curLine.charAt(i) == ' ')
						{
							vertexTextureNormal++;
							if(vertexTextureNormal == 3)
							{
								vertexTextureNormal = 0;
							}
							if(curLine.charAt(i) == ' ')
							{
								xyz++;
								vertexTextureNormal = 0;
							}
							if(vertexTextureNormal == 1)
							{
								hasTex = true;
							}
						}
						else if(vertexTextureNormal == 0)
						{
							if(xyz == 0)
							{
								firstCoords += curLine.charAt(i);
							}
							else if(xyz == 1)
							{
								secondCoords += curLine.charAt(i);
							}
							else if(xyz == 2)
							{
								thirdCoords += curLine.charAt(i);
							}
						}
						else if(vertexTextureNormal == 1)
						{
							if(xyz == 0)
							{
								firstTex += curLine.charAt(i);
							}
							else if(xyz == 1)
							{
								secondTex += curLine.charAt(i);
							}
							else if(xyz == 2)
							{
								thirdTex += curLine.charAt(i);
							}
						}
					}
					
					//Adds to the ArrayList in the ArrayList
					faceVectorAllocate.get(Integer.parseInt(firstCoords) - 1).add(orderedVertices.size() - 1);
					faceVectorAllocate.get(Integer.parseInt(secondCoords) - 1).add(orderedVertices.size() - 1);
					faceVectorAllocate.get(Integer.parseInt(thirdCoords) - 1).add(orderedVertices.size() - 1);

					for(int i = 0; i < 3; i++)
					{
						orderedVertices.get(orderedVertices.size() - 1)[i] = vertexList.get(Integer.parseInt(firstCoords) - 1)[i];
					}
					for(int i = 3; i < 6; i++)
					{
						orderedVertices.get(orderedVertices.size() - 1)[i] = vertexList.get(Integer.parseInt(secondCoords) - 1)[i - 3];
					}
					for(int i = 6; i < 9; i++)
					{
						orderedVertices.get(orderedVertices.size() - 1)[i] = vertexList.get(Integer.parseInt(thirdCoords) - 1)[i - 6];
					}

					if(hasTex && !firstTex.equals(""))
					{                                               
						for(int i = 0; i < 2; i++)
						{
							orderedTextures.get(orderedVertices.size() - 1)[i] = textureList.get(Integer.parseInt(firstTex) - 1)[i];
						}
						for(int i = 2; i < 4; i++)
						{
							orderedTextures.get(orderedVertices.size() - 1)[i] = textureList.get(Integer.parseInt(secondTex) - 1)[i - 2];
						}
						for(int i = 4; i < 6; i++)
						{
							orderedTextures.get(orderedVertices.size() - 1)[i] = textureList.get(Integer.parseInt(thirdTex) - 1)[i - 4];
						}
					}
					Vector a = new Vector(orderedVertices.get(orderedVertices.size() - 1)[0], orderedVertices.get(orderedVertices.size() - 1)[1], orderedVertices.get(orderedVertices.size() - 1)[2]);
					Vector b = new Vector(orderedVertices.get(orderedVertices.size() - 1)[3], orderedVertices.get(orderedVertices.size() - 1)[4], orderedVertices.get(orderedVertices.size() - 1)[5]);
					Vector c = new Vector(orderedVertices.get(orderedVertices.size() - 1)[6], orderedVertices.get(orderedVertices.size() - 1)[7], orderedVertices.get(orderedVertices.size() - 1)[8]);
					Vector[] Vertices = {a, b, c};
					faceList.add(new Face(faceList.size(), Vertices));
				}
				else if(curLine.length() > 7)
				{
					//Searches the material file (Should be deprecated, is not stable)
					if(curLine.startsWith("mtllib "))
					{
						Scanner texScanner = null;
						try
						{
							texScanner = new Scanner(new File(curLine.substring(7, curLine.length())));
						}
						catch (FileNotFoundException e)
						{
							e.printStackTrace();
						}
						String curTexLine = texScanner.next();
						while(texScanner.hasNext())
						{
							curTexLine = texScanner.next();
							if(curTexLine.length() > 6)
							{
								if(curTexLine.substring(0, 7).equals("map_Ka")) //Simple texture map, in upper right plane
								{
									try
									{
										ambientTex = TextureLoader.getTexture(curTexLine.substring(curTexLine.length() - 3, curTexLine.length()),
												new FileInputStream(new File(curTexLine.substring(8, curTexLine.length())))); //Often, file is not found
									}
									catch (FileNotFoundException e)
									{
										e.printStackTrace();
									}
									catch (IOException e)
									{
										e.printStackTrace();
									}
								}
							}
						}
					}
				}
			}
		}
		calculateNeighbors();
		calculateNormals();
		averageNormals();
		assignNormals(name);
	}

	//Same as constructor above, except all vertices are offset by the prism, which means that the model has to have it's top-left-deep corner at <0,0,0> and fit in a 1*1*1 cube
	public ObjectCoords(String name, Prism offset)
	{
		try
		{
			scan = new Scanner(new File((name + ".obj")));
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		}
		//Reads until EOF, Scanner is easier to use than a BufferedReader, although changes may be made for improved speed.
		while(scan.hasNext())
		{
			String curLine = scan.nextLine();
			if(curLine.length() > 2 && !curLine.startsWith("mtl"))
			{
				int curChar = 2; //Starts after declaration of the type of data is being inputed (e.g. "v " or "f ", later "vt " will add 1 to curChar)
				if(curLine.substring(0, 2).equals("v "))
				{
					faceVectorAllocate.add(new ArrayList<Integer>());
					vertexList.add(new double[3]); //Adding to the real unordered vertexList
					String coordX = "", coordY = "", coordZ = "";
					//The following reads in a line such as: "v 1 2 3". Should be upgraded to use a StringTokenizer
					while(curLine.charAt(curChar) != ' ')
					{
						coordX += curLine.charAt(curChar);
						curChar++;
					}
					vertexList.get(vertexList.size() - 1)[0] = Double.parseDouble(coordX) * offset.getSizeX() + offset.getX(); //Assigns x coordinate to unordered list
					curChar++;
					while(curLine.charAt(curChar) != ' ')
					{
						coordY += curLine.charAt(curChar);
						curChar++;
					}
					vertexList.get(vertexList.size() - 1)[1] = Double.parseDouble(coordY) * offset.getSizeY() + offset.getY(); //Assigns y
					curChar++;
					while(curChar < curLine.length())
					{
						coordZ += curLine.charAt(curChar);
						curChar++;
					}
					vertexList.get(vertexList.size() - 1)[2] = Double.parseDouble(coordZ) * offset.getSizeZ() + offset.getZ(); //Assigns z
				}
				//Assigns vertex texture coordinates
				else if(curLine.substring(0, 3).equals("vt "))
				{
					curChar++; //Adds one character because "vt " has one more character
					textureList.add(new double[2]); //Only two coordinates because textures are assigned to a two-dimensional image
					String coordX = "", coordY = "";
					//Like previous, should be upgraded to a StringTokenizer
					while(curLine.charAt(curChar) != ' ')
					{
						coordX += curLine.charAt(curChar);
						curChar++;
					}
					textureList.get(textureList.size() - 1)[0] = Double.parseDouble(coordX);
					while(curChar < curLine.length())
					{
						coordY += curLine.charAt(curChar);
						curChar++;
					}
					textureList.get(textureList.size() - 1)[1] = Double.parseDouble(coordY);
				}
				//Assigns textures and vertices to their corresponding face
				else if(curLine.substring(0, 2).equals("f "))
				{
					String firstCoords = "", secondCoords = "", thirdCoords = "";
					String firstTex = "", secondTex = "", thirdTex = "";
					int xyz = 0;
					int vertexTextureNormal = 0;
					boolean hasTex = false;
					orderedVertices.add(new double[9]);
					orderedTextures.add(new double[6]);
					//Searches through and assigns vertices to texture coordinates, like so: "f v/t v/t v/t"
					for(int i = 2; i < curLine.length(); i++)
					{
						if(curLine.charAt(i) == '/' || curLine.charAt(i) == ' ')
						{
							vertexTextureNormal++;
							if(vertexTextureNormal == 3)
							{
								vertexTextureNormal = 0;
							}
							if(curLine.charAt(i) == ' ')
							{
								xyz++;
								vertexTextureNormal = 0;
							}
							if(vertexTextureNormal == 1)
							{
								hasTex = true;
							}
						}
						else if(vertexTextureNormal == 0)
						{
							if(xyz == 0)
							{
								firstCoords += curLine.charAt(i);
							}
							else if(xyz == 1)
							{
								secondCoords += curLine.charAt(i);
							}
							else if(xyz == 2)
							{
								thirdCoords += curLine.charAt(i);
							}
						}
						else if(vertexTextureNormal == 1)
						{
							if(xyz == 0)
							{
								firstTex += curLine.charAt(i);
							}
							else if(xyz == 1)
							{
								secondTex += curLine.charAt(i);
							}
							else if(xyz == 2)
							{
								thirdTex += curLine.charAt(i);
							}
						}
					}
					
					//Adds to the ArrayList in the ArrayList
					faceVectorAllocate.get(Integer.parseInt(firstCoords) - 1).add(orderedVertices.size() - 1);
					faceVectorAllocate.get(Integer.parseInt(secondCoords) - 1).add(orderedVertices.size() - 1);
					faceVectorAllocate.get(Integer.parseInt(thirdCoords) - 1).add(orderedVertices.size() - 1);

					for(int i = 0; i < 3; i++)
					{
						orderedVertices.get(orderedVertices.size() - 1)[i] = vertexList.get(Integer.parseInt(firstCoords) - 1)[i];
					}
					for(int i = 3; i < 6; i++)
					{
						orderedVertices.get(orderedVertices.size() - 1)[i] = vertexList.get(Integer.parseInt(secondCoords) - 1)[i - 3];
					}
					for(int i = 6; i < 9; i++)
					{
						orderedVertices.get(orderedVertices.size() - 1)[i] = vertexList.get(Integer.parseInt(thirdCoords) - 1)[i - 6];
					}

					if(hasTex && !firstTex.equals(""))
					{                                               
						for(int i = 0; i < 2; i++)
						{
							orderedTextures.get(orderedVertices.size() - 1)[i] = textureList.get(Integer.parseInt(firstTex) - 1)[i];
						}
						for(int i = 2; i < 4; i++)
						{
							orderedTextures.get(orderedVertices.size() - 1)[i] = textureList.get(Integer.parseInt(secondTex) - 1)[i - 2];
						}
						for(int i = 4; i < 6; i++)
						{
							orderedTextures.get(orderedVertices.size() - 1)[i] = textureList.get(Integer.parseInt(thirdTex) - 1)[i - 4];
						}
					}
					Vector a = new Vector(orderedVertices.get(orderedVertices.size() - 1)[0], orderedVertices.get(orderedVertices.size() - 1)[1], orderedVertices.get(orderedVertices.size() - 1)[2]);
					Vector b = new Vector(orderedVertices.get(orderedVertices.size() - 1)[3], orderedVertices.get(orderedVertices.size() - 1)[4], orderedVertices.get(orderedVertices.size() - 1)[5]);
					Vector c = new Vector(orderedVertices.get(orderedVertices.size() - 1)[6], orderedVertices.get(orderedVertices.size() - 1)[7], orderedVertices.get(orderedVertices.size() - 1)[8]);
					Vector[] Vertices = {a, b, c};
					faceList.add(new Face(faceList.size(), Vertices));
				}
				else if(curLine.length() > 7)
				{
					//Searches the material file (Should be deprecated, is not stable)
					if(curLine.startsWith("mtllib "))
					{
						Scanner texScanner = null;
						try
						{
							texScanner = new Scanner(new File(curLine.substring(7, curLine.length())));
						}
						catch (FileNotFoundException e)
						{
							e.printStackTrace();
						}
						String curTexLine = texScanner.next();
						while(texScanner.hasNext())
						{
							curTexLine = texScanner.next();
							if(curTexLine.length() > 6)
							{
								if(curTexLine.substring(0, 7).equals("map_Ka")) //Simple texture map, in upper right plane
								{
									try
									{
										ambientTex = TextureLoader.getTexture(curTexLine.substring(curTexLine.length() - 3, curTexLine.length()),
												new FileInputStream(new File(curTexLine.substring(8, curTexLine.length())))); //Often, file is not found
									}
									catch (FileNotFoundException e)
									{
										e.printStackTrace();
									}
									catch (IOException e)
									{
										e.printStackTrace();
									}
								}
							}
						}
					}
				}
			}
		}
		calculateNeighbors();
		calculateNormals();
		averageNormals();
		assignNormals(name);
	}
	
	//Creates normals per face, assuming counter-clockwise direction of creation of vertices (Normalized cross product)
	public void calculateNormals()
	{
		for(double[] face : orderedVertices)
		{
			double[] vec1 = {face[3] - face[0], face[4] - face[1], face[5] - face[2]};
			double[] vec2 = {face[6] - face[0], face[7] - face[1], face[8] - face[2]};

			double determinantX = (vec1[1] * vec2[2]) - (vec2[1] * vec1[2]);
			double determinantY = -1.0 * ((vec1[0] * vec2[2]) - (vec2[0] * vec1[2]));
			double determinantZ = (vec1[0] * vec2[1]) - (vec2[0] * vec1[1]);

			double[] normalVec = {determinantX, determinantY, determinantZ};
			double magnitude = Math.sqrt((determinantX * determinantX) + (determinantY * determinantY) + (determinantZ * determinantZ));
			normalVec[0] = normalVec[0] / magnitude;
			normalVec[1] = normalVec[1] / magnitude;
			normalVec[2] = normalVec[2] / magnitude;

			faceNormals.add(new double[3]);
			faceNormals.get(faceNormals.size() - 1)[0] = normalVec[0];
			faceNormals.get(faceNormals.size() - 1)[1] = normalVec[1];
			faceNormals.get(faceNormals.size() - 1)[2] = normalVec[2];
		}
	}

	public void averageNormals()
	{
		for(ArrayList<Integer> faces : faceVectorAllocate)
		{
			double sumX = 0, sumY = 0, sumZ = 0;
			for(int curFace : faces)
			{
				sumX += faceNormals.get(curFace)[0];
				sumY += faceNormals.get(curFace)[1];
				sumZ += faceNormals.get(curFace)[2];
			}

			double averageX = sumX / faces.size();
			double averageY = sumY / faces.size();
			double averageZ = sumZ / faces.size();

			double magnitude = Math.sqrt((averageX * averageX) + (averageY * averageY) + (averageZ * averageZ));
			averageX /= magnitude;
			averageY /= magnitude;
			averageZ /= magnitude;
			
			vertexNormals.add(new double[3]);
			vertexNormals.get(vertexNormals.size() - 1)[0] = averageX;
			vertexNormals.get(vertexNormals.size() - 1)[1] = averageY;
			vertexNormals.get(vertexNormals.size() - 1)[2] = averageZ;
		}
	}

	public void assignNormals(String name)
	{
		Scanner scan = null;
		try
		{
			scan = new Scanner(new File(name + ".obj"));
		}
		catch (FileNotFoundException e)
		{
			e.printStackTrace();
		}
		while(scan.hasNext())
		{
			String curLine = scan.nextLine();
			if(curLine.length() > 2)
			{
				if(curLine.substring(0, 2).equals("f "))
				{
					String firstCoords = "", secondCoords = "", thirdCoords = "";
					int xyz = 0;
					int vertexTextureNormal = 0;
					for(int i = 2; i < curLine.length(); i++)
					{
						if(curLine.charAt(i) == '/' || curLine.charAt(i) == ' ')
						{
							vertexTextureNormal++;
							if(vertexTextureNormal == 3)
							{
								vertexTextureNormal = 0;
							}
							if(curLine.charAt(i) == ' ')
							{
								xyz++;
								vertexTextureNormal = 0;
							}
						}
						else if(vertexTextureNormal == 0)
						{
							if(xyz == 0)
							{
								firstCoords += curLine.charAt(i);
							}
							else if(xyz == 1)
							{
								secondCoords += curLine.charAt(i);
							}
							else if(xyz == 2)
							{
								thirdCoords += curLine.charAt(i);
							}
						}
					}
					int firstVert = Integer.parseInt(firstCoords), secondVert = Integer.parseInt(secondCoords), thirdVert = Integer.parseInt(thirdCoords);
					vertexFaceNormals.add(new double[9]);
					for(int i = 0; i < 3; i++)
					{                                       
						vertexFaceNormals.get(vertexFaceNormals.size() - 1)[i] = vertexNormals.get(firstVert - 1)[i];
					}
					for(int i = 0; i < 3; i++)
					{                                       
						vertexFaceNormals.get(vertexFaceNormals.size() - 1)[i + 3] = vertexNormals.get(secondVert - 1)[i];
					}
					for(int i = 0; i < 3; i++)
					{                                       
						vertexFaceNormals.get(vertexFaceNormals.size() - 1)[i + 6] = vertexNormals.get(thirdVert - 1)[i];
					}
				}
			}
		}
	}

	public void calculateNeighbors()
	{
		for(Face face : faceList)
		{
			int curIndex = face.getId();
			Vector v1 = face.getVertices()[0];
			Vector v2 = face.getVertices()[1];
			Vector v3 = face.getVertices()[2];
			for(Face metaFace : faceList)
			{
				Vector new1 = metaFace.getVertices()[0];
				Vector new2 = metaFace.getVertices()[1];
				Vector new3 = metaFace.getVertices()[2];
				boolean firstSame = (v1.getX() == new1.getX() && v1.getY() == new1.getY() && v1.getZ() == new1.getZ());
				boolean secondSame = (v2.getX() == new2.getX() && v2.getY() == new2.getY() && v2.getZ() == new2.getZ());
				boolean thirdSame = (v3.getX() == new3.getX() && v3.getY() == new3.getY() && v3.getZ() == new3.getZ());
				if(firstSame && secondSame && thirdSame)
				{
					for(int i = 0; i < 4 && metaFace.getNeighbors()[i] != -1; i++)
					{
						metaFace.getNeighbors()[i] = curIndex;
					}
					for(int i = 0;  i < 4 && face.getNeighbors()[i] != -1; i++)
					{
						face.getNeighbors()[i] = metaFace.getId();
					}
				}
			}
		}
	}

	public void assignNormalsToObjects()
	{
		for(Face face : faceList)
		{
			face.getNormals()[0].setX(vertexFaceNormals.get(face.getId())[0]);
			face.getNormals()[0].setY(vertexFaceNormals.get(face.getId())[1]);
			face.getNormals()[0].setZ(vertexFaceNormals.get(face.getId())[2]);
			face.getNormals()[1].setX(vertexFaceNormals.get(face.getId())[3]);
			face.getNormals()[1].setY(vertexFaceNormals.get(face.getId())[4]);
			face.getNormals()[1].setZ(vertexFaceNormals.get(face.getId())[5]);
			face.getNormals()[2].setX(vertexFaceNormals.get(face.getId())[6]);
			face.getNormals()[2].setY(vertexFaceNormals.get(face.getId())[7]);
			face.getNormals()[2].setZ(vertexFaceNormals.get(face.getId())[8]);
		}
	}

	public void calculatePlaneEquation()
	{
		for(Face face : faceList)
		{
			double a = faceNormals.get(face.getId())[0];
			double b = faceNormals.get(face.getId())[1];
			double c = faceNormals.get(face.getId())[2];
			double d = (-a * face.getVertices()[0].getX()) - (b * face.getVertices()[0].getY()) - (c * face.getVertices()[0].getZ());
			face.setPlane(new Plane(a, b, c, d));
		}
	}
}